2015年,Facebook推出了GraphQL(Graph-Query-Language)查询语言。到目前为止,IBM、Twitter、Walmart Labs、纽约时报、Coursera等很多公司已经在内部从RESTful转向GraphQL API。
作为一种查询语言,GraphQL具有以下特点:
(1)无需关心如何更新文档,所有的查询(query)和变更会自动形成文档(cchema)。
(2)无需获取整个数据集,通过schema与resolver(处理器)之间的映射关系,由对应的resolver去获取数据,将结果返回给前端,从而可以编写仅仅返回所请求数据的查询。
(3)对前端提供统一的访问点。从不同的API中获取数据并非易事,GraphQL支持将所有API进行拼接。
爱奇艺号技术团队在实施微服务化的过程中,受到Forrester Research提出的低代码开发(Low-Code:即无需编码或通过少量代码就可以快速生成应用程序的开发理念)的启发,基于GraphQL构建BFF(Backend for Frontends),帮助开发人员用拖拽式操作,直观地创建出一个供前端调用的API,本文将对实施过程中的经验总结进行叙述。
与 RESTful API 一样,GraphQL API设计用于处理 HTTP 请求并为这些请求提供响应。REST API 构建在请求方法和端点之间的连接上,而 GraphQL API 被设计为只通过一个端点,始终使用 POST 请求进行查询,其 URL 通常是xxx.com/graphql。图1-1为GraphQL部署架构图,可以看到它处于系统“中间层”。
GraphQL 全称叫 Graph Query Language,官方宣传语是“为你的 API 量身定制的查询语言”,用传统的方式来解释就是:将你所有后端 API 组成的集合看成一个数据库,用户终端发送一个查询语句,你的 GraphQL 服务解析这条语句并通过一系列规则从你的“API 数据库”里面将查询的数据结果返回给终端,而 GraphQL 就相当于这个系统的一个查询语言,像 SQL 之于 MySQL 一样。GraphQL执行过程如图1-2所示:
图1-2是GraphQL 执行的大致流程,第一步去验证查询语句是否符合GraphQL的schema规范,确认查询内容的合法性,第二步生成执行的上下文,关键点在第三步和第四步,第三步是获取查询语句所需要查询的字段,这里叫 fields,所有需要查询的字段可以在查询语句里拿到,这就是 GraphQL 如何做到避免返回冗余数据的。拿到所有需要查询的字段后,第四步针对每一个字段去执行它的 resolver,可以从 resolver 返回的数据里面拿到字段对应的数据,最后是格式化结果并返回。重点是第四步,展开说明一下,如图1-3、图1-4所示:
在GraphQL里面有一个概念叫类型 (type),每一个类型下面对应的是一个或多个字段(field),每个字段都会绑定一个处理器(resolver),这个 resolver 的作用就是获取字段对应的数据。对应到图1-4所示,UserInfo这个类型,它有三个字段:nickName、contractNo、fansNumber。每个字段都对应一个resolver,resolver 需要被开发者重新定义,否则会报错。所以UserInfo下的三个字段nickName、contractNo、fansNumber需要通过实现各自resolver来分别从用户微服务、合同微服务、粉丝微服务去获取用户信息、合同信息和粉丝信息,然后再聚合返回,这样就达到了使用 GraphQL 进行数据拼接的目的。爱奇艺号在实施微服务化的过程中,加入了BFF的前后端架构,如图2-1所示:
从图中可以看出,BFF作为前后端的中间层服务。主要的业务逻辑都封装在BFF层,前端通过BFF进行访问,减少微服务之间的相互调用。BFF层通过REST API方式提供服务,随着服务的增多,提供的接口越来越多,这会导致REST API越来越冗余。对于前端而言,有的API粒度较粗不满足需求;有的API又粒度太细,不仅增加了响应时间,还会造成流量的浪费。对于后端而言,前端需要的数据往往在不同的地方具有相似性,但却又不尽相同,比如:针对用户信息,有些地方需要用户简要的基础信息和详细的视频信息,而有些地方却需要用户详细的基础信息和简要的视频信息。这往往需要开发不同的接口去满足各种定制需求,增加了开发人员的工作量,提升了开发工作的重复度。GraphQL与Rest API对比:
| 性能 | 文档 | 调试 | 学习成本 |
GraphQL | 性能好,申明式获取,非常直观和精准,减少不必要数据网络传输 | 代码即文档 | 前端能快速感知,强类型,避免出错 | 高 |
Rest API | 缺乏弹性,大多数情况会获取额外数据 | 使用Swagger等 | 后端配合排查 | 低 |
从表2-1中的对比可以看出,GraphQL相对于Rest API方式,性能更好,能有效减少前后端开发沟通成本。但是Facebook的官方只有JS版本实现,查询方式和Rest API也有所不同(如表2-2所示),对于老项目有一定的迁移、学习成本。
接下来,本文将主要探讨如何基于graphql-java,做到减少迁移成本的同时,又能提升后端开发人员的效率,避免重复开发。爱奇艺号API生成平台,是一个低代码平台。由于爱奇艺号的技术栈主要基于Java,所以使用的是GraphQL的 Java实现。基于graphql-java,API生成平台主要做了以下功能优化及增强。
(2)动态接入监控:动态生成的API,与其他普通接口一样支持Prometheus监控,保证监控的灵活性和服务的稳定性。(3)灵活配置:可以动态生成GraphQL的schema,方便后端接入新服务。(4)可视化API管理平台:API接口提供可视化操作,方便查看、新增、修改和重用。graphql-java通过Spring的封装,位于整个架构的网关层或BFF层。项目user-info-graphQL依赖graphql-java-spring,支持Rest API请求。平台的整体架构图如图3-1所示:
User-info-graphQL的服务流程图如图3-2所示。客户端通过graphql/前缀的Rest API方式请求,后端通过前缀与GraphQL Query绑定,从DB获取映射关系,最终转换成GraphQL支持的查询语法。
在user-info-graphQL项目中,原本是通过template url来匹配任意自定义url;导致监控平台只能显示template url的请求信息,如图3-3所示。这个问题可以通过重写spring-boot-actuator中获取tags的方法,将真实的url请求信息暴露到Spring boot的/actuator/prometheus端点这个方法来解决,如图3-4所示。
通过暴露的监控端点接入Prometheus,实现对新生成的API进行实时动态监控,示例效果如图3-5所示:
为了方便后端快速接入新增微服务,达到支持API动态扩展目的。项目中通过Velocity定义schema模板,通过Java注解、反射机制动态生成graphqls模板文件,如图3-6所示:
图3-6中的GraphQL schema模板,支持通过用户UID查询用户信息。用户信息是由多个微服务聚合而成,采用异步调用多个微服务并行获取数据。基于此模板,用户只需要实现SPI定义好的接口,就能实现对新增微服务的支持。通过API生成管理平台,开发人员可以实现API接口的可视化配置、生成、动态监控等功能,达到开箱即用的效果,极大提升开发和运维效率。通过GraphQL动态构建BFF服务层API,聚合不同的微服务,相比于Rest API方式,能够减少后端重复开发,加快响应前端需求。后端开发人员只需要开发维护新增微服务,并通过SPI方式,增加BFF层对新增微服务的支持即可。通过在爱奇艺号后端微服务引入GraphQL构建BFF服务层,可以达成以下效果:
- 便于监控:新增BFF层API接口,通过支持Prometheus端点监控,无开发成本。
- 支持系统高吞吐量:BFF服务、微服务都是基于docker部署,支持QPS动态扩容,能够支持高并发。
- 便于维护和扩展:基于GraphQL构建的BFF层,API接口动态生成,层次清晰,更易维护、扩展。
未来规划:
随着BFF端对API请求的多样化,需要动态支持新方法扩展及监控。目前API与请求的映射关系持久化在MySQL中,需要支持集群和高性能,后续逐步迁移到ZK或Redis中,并缓存到本地。
随着云原生和K8s的兴起,基于K8s部署的Go服务,更易扩容和维护。基于Java实现的GraphQL,如果迁移到K8s上部署,很难实现快速扩缩容的效果。而graphql-go在github上的star高达7k,可见热度极高。如果基于Go实现BFF端,API与请求的映射关系可以存储于K8s的Pod配置文件中,并且通过一个API部署一类Pod,进行服务隔离,可以更高程度的保证服务稳定。